Safe passing of structured data
To support passing JavaScript objects, including platform objects, across realm boundaries, this specification defines the following infrastructure for serializing and deserializing objects, including in some cases transferring the underlying data instead of copying it. Collectively this serialization/deserialization process is known as "structured cloning", although most APIs perform separate serialization and deserialization steps. (With the notable exception being the <a href="https://html.spec.whatwg.org/multipage/structured-data.html#dom-structuredclone">structuredClone()</a>
method.)
This section uses the terminology and typographic conventions from the JavaScript specification. [JAVASCRIPT]
2.7.1 Serializable objects
MDN
Serializable objects support being serialized, and later deserialized, in a way that is independent of any given realm. This allows them to be stored on disk and later restored, or cloned across agent and even agent cluster boundaries.
Not all objects are serializable objects, and not all aspects of objects that are serializable objects are necessarily preserved when they are serialized.
Platform objects can be serializable objects if their primary interface is decorated with the [Serializable]
IDL extended attribute. Such interfaces must also define the following algorithms:
serialization steps, taking a platform object value, a Record serialized, and a boolean forStorageA set of steps that serializes the data in value into fields of serialized. The resulting data serialized into serialized must be independent of any realm.
These steps may throw an exception if serialization is not possible.
These steps may perform a sub-serialization to serialize nested data structures. They should not call StructuredSerialize directly, as doing so will omit the important memory argument.
The introduction of these steps should omit mention of the forStorage argument if it is not relevant to the algorithm.
deserialization steps, taking a Record serialized, a platform object value, and a realm targetRealmA set of steps that deserializes the data in serialized, using it to set up value as appropriate. value will be a newly-created instance of the platform object type in question, with none of its internal data set up; setting that up is the job of these steps.
These steps may throw an exception if deserialization is not possible.
These steps may perform a sub-deserialization to deserialize nested data structures. They should not call StructuredDeserialize directly, as doing so will omit the important targetRealm and memory arguments.
It is up to the definition of individual platform objects to determine what data is serialized and deserialized by these steps. Typically the steps are very symmetric.
The <a href="https://html.spec.whatwg.org/multipage/structured-data.html#serializable">[Serializable]</a>
extended attribute must take no arguments, and must only appear on an interface. It must not appear more than once on an interface.
For a given platform object, only the object's primary interface is considered during the (de)serialization process. Thus, if inheritance is involved in defining the interface, each <a href="https://html.spec.whatwg.org/multipage/structured-data.html#serializable">[Serializable]</a>
-annotated interface in the inheritance chain needs to define standalone serialization steps and deserialization steps, including taking into account any important data that might come from inherited interfaces.
Let's say we were defining a platform object Person
, which had associated with it two pieces of associated data:
- a name value, which is a string; and
- a best friend value, which is either another
Person
instance or null.
We could then define Person
instances to be serializable objects by annotating the Person
interface with the <a href="https://html.spec.whatwg.org/multipage/structured-data.html#serializable">[Serializable]</a>
extended attribute, and defining the following accompanying algorithms:
serialization steps1. Set serialized.[[Name]] to value's associated name value.
- Let serializedBestFriend be the sub-serialization of value's associated best friend value.
- Set serialized.[[BestFriend]] to serializedBestFriend.
deserialization steps1. Set value's associated name value to serialized.[[Name]].
- Let deserializedBestFriend be the sub-deserialization of serialized.[[BestFriend]].
- Set value's associated best friend value to deserializedBestFriend.
Objects defined in the JavaScript specification are handled by the StructuredSerialize abstract operation directly.
Originally, this specification defined the concept of "cloneable objects", which could be cloned from one realm to another. However, to better specify the behavior of certain more complex situations, the model was updated to make the serialization and deserialization explicit.
2.7.2 Transferable objects
Transferable objects support being transferred across agents. Transferring is effectively recreating the object while sharing a reference to the underlying data and then detaching the object being transferred. This is useful to transfer ownership of expensive resources. Not all objects are transferable objects and not all aspects of objects that are transferable objects are necessarily preserved when transferred.
Transferring is an irreversible and non-idempotent operation. Once an object has been transferred, it cannot be transferred, or indeed used, again.
Platform objects can be transferable objects if their primary interface is decorated with the [Transferable]
IDL extended attribute. Such interfaces must also define the following algorithms:
transfer steps, taking a platform object value and a Record dataHolderA set of steps that transfers the data in value into fields of dataHolder. The resulting data held in dataHolder must be independent of any realm.
These steps may throw an exception if transferral is not possible.
transfer-receiving steps, taking a Record dataHolder and a platform object valueA set of steps that receives the data in dataHolder, using it to set up value as appropriate. value will be a newly-created instance of the platform object type in question, with none of its internal data set up; setting that up is the job of these steps.
These steps may throw an exception if it is not possible to receive the transfer.
It is up to the definition of individual platform objects to determine what data is transferred by these steps. Typically the steps are very symmetric.
The <a href="https://html.spec.whatwg.org/multipage/structured-data.html#transferable">[Transferable]</a>
extended attribute must take no arguments, and must only appear on an interface. It must not appear more than once on an interface.
For a given platform object, only the object's primary interface is considered during the transferring process. Thus, if inheritance is involved in defining the interface, each <a href="https://html.spec.whatwg.org/multipage/structured-data.html#transferable">[Transferable]</a>
-annotated interface in the inheritance chain needs to define standalone transfer steps and transfer-receiving steps, including taking into account any important data that might come from inherited interfaces.
Platform objects that are transferable objects have a [[Detached]] internal slot. This is used to ensure that once a platform object has been transferred, it cannot be transferred again.
Objects defined in the JavaScript specification are handled by the StructuredSerializeWithTransfer abstract operation directly.
2.7.3 StructuredSerializeInternal ( value, forStorage [ , memory ] )
The StructuredSerializeInternal abstract operation takes as input a JavaScript value value and serializes it to a realm-independent form, represented here as a Record. This serialized form has all the information necessary to later deserialize into a new JavaScript value in a different realm.
This process can throw an exception, for example when trying to serialize un-serializable objects.
If memory was not supplied, let memory be an empty map. The purpose of the memory map is to avoid serializing objects twice. This ends up preserving cycles and the identity of duplicate objects in graphs.
If memory[value] exists, then return memory[value].
Let deep be false.
If Type(value) is Undefined, Null, Boolean, Number, BigInt, or String, then return { : "primitive", [[Value]]: value }.
If Type(value) is Symbol, then throw a "
DataCloneError
"<a data-x-internal="domexception" href="https://webidl.spec.whatwg.org/#dfn-DOMException">DOMException</a>
.Let serialized be an uninitialized value.
If value has a [[BooleanData]] internal slot, then set serialized to { : "Boolean", [[BooleanData]]: value.[[BooleanData]] }.
Otherwise, if value has a [[NumberData]] internal slot, then set serialized to { : "Number", [[NumberData]]: value.[[NumberData]] }.
Otherwise, if value has a [[BigIntData]] internal slot, then set serialized to { : "BigInt", [[BigIntData]]: value.[[BigIntData]] }.
Otherwise, if value has a [[StringData]] internal slot, then set serialized to { : "String", [[StringData]]: value.[[StringData]] }.
Otherwise, if value has a [[DateValue]] internal slot, then set serialized to { : "Date", [[DateValue]]: value.[[DateValue]] }.
Otherwise, if value has a [[RegExpMatcher]] internal slot, then set serialized to { : "RegExp", [[RegExpMatcher]]: value.[[RegExpMatcher]], [[OriginalSource]]: value.[[OriginalSource]], [[OriginalFlags]]: value.[[OriginalFlags]] }.
Otherwise, if value has an [[ArrayBufferData]] internal slot, then:
- If IsSharedArrayBuffer(value) is true, then:
- If the current settings object's cross-origin isolated capability is false, then throw a "
DataCloneError
"<a data-x-internal="domexception" href="https://webidl.spec.whatwg.org/#dfn-DOMException">DOMException</a>
. This check is only needed when serializing (and not when deserializing) as the cross-origin isolated capability cannot change over time and a<a data-x-internal="sharedarraybuffer" href="https://tc39.es/ecma262/#sec-sharedarraybuffer-objects">SharedArrayBuffer</a>
cannot leave an agent cluster. - If forStorage is true, then throw a "
DataCloneError
"<a data-x-internal="domexception" href="https://webidl.spec.whatwg.org/#dfn-DOMException">DOMException</a>
. - If value has an [[ArrayBufferMaxByteLength]] internal slot, then set serialized to { : "GrowableSharedArrayBuffer", [[ArrayBufferData]]: value.[[ArrayBufferData]], [[ArrayBufferByteLengthData]]: value.[[ArrayBufferByteLengthData]], [[ArrayBufferMaxByteLength]]: value.[[ArrayBufferMaxByteLength]], [[AgentCluster]]: the surrounding agent's agent cluster }.
- Otherwise, set serialized to { : "SharedArrayBuffer", [[ArrayBufferData]]: value.[[ArrayBufferData]], [[ArrayBufferByteLength]]: value.[[ArrayBufferByteLength]], [[AgentCluster]]: the surrounding agent's agent cluster }.
- If the current settings object's cross-origin isolated capability is false, then throw a "
- Otherwise:
- If IsDetachedBuffer(value) is true, then throw a "
DataCloneError
"<a data-x-internal="domexception" href="https://webidl.spec.whatwg.org/#dfn-DOMException">DOMException</a>
. - Let size be value.[[ArrayBufferByteLength]].
- Let dataCopy be ? CreateByteDataBlock(size). This can throw a
<a data-x-internal="js-rangeerror" href="https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-rangeerror">RangeError</a>
exception upon allocation failure. - Perform CopyDataBlockBytes(dataCopy, 0, value.[[ArrayBufferData]], 0, size).
- If value has an [[ArrayBufferMaxByteLength]] internal slot, then set serialized to { : "ResizableArrayBuffer", [[ArrayBufferData]]: dataCopy, [[ArrayBufferByteLength]]: size, [[ArrayBufferMaxByteLength]]: value.[[ArrayBufferMaxByteLength]] }.
- Otherwise, set serialized to { : "ArrayBuffer", [[ArrayBufferData]]: dataCopy, [[ArrayBufferByteLength]]: size }.
- If IsDetachedBuffer(value) is true, then throw a "
- If IsSharedArrayBuffer(value) is true, then:
Otherwise, if value has a [[ViewedArrayBuffer]] internal slot, then:
- If IsArrayBufferViewOutOfBounds(value) is true, then throw a "
DataCloneError
"<a data-x-internal="domexception" href="https://webidl.spec.whatwg.org/#dfn-DOMException">DOMException</a>
. - Let buffer be the value of value's [[ViewedArrayBuffer]] internal slot.
- Let bufferSerialized be ? StructuredSerializeInternal(buffer, forStorage, memory).
- Assert: bufferSerialized. is "ArrayBuffer", "ResizableArrayBuffer", "SharedArrayBuffer", or "GrowableSharedArrayBuffer".
- If value has a [[DataView]] internal slot, then set serialized to { : "ArrayBufferView", [[Constructor]]: "DataView", [[ArrayBufferSerialized]]: bufferSerialized, [[ByteLength]]: value.[[ByteLength]], [[ByteOffset]]: value.[[ByteOffset]] }.
- Otherwise:
- Assert: value has a [[TypedArrayName]] internal slot.
- Set serialized to { : "ArrayBufferView", [[Constructor]]: value.[[TypedArrayName]], [[ArrayBufferSerialized]]: bufferSerialized, [[ByteLength]]: value.[[ByteLength]], [[ByteOffset]]: value.[[ByteOffset]], [[ArrayLength]]: value.[[ArrayLength]] }.
- If IsArrayBufferViewOutOfBounds(value) is true, then throw a "
Otherwise, if value has [[MapData]] internal slot, then:
- Set serialized to { : "Map", [[MapData]]: a new empty List }.
- Set deep to true.
Otherwise, if value has [[SetData]] internal slot, then:
- Set serialized to { : "Set", [[SetData]]: a new empty List }.
- Set deep to true.
Otherwise, if value has an [[ErrorData]] internal slot and value is not a platform object, then:
- Let name be ? Get(value, "name").
- If name is not one of "Error", "EvalError", "RangeError", "ReferenceError", "SyntaxError", "TypeError", or "URIError", then set name to "Error".
- Let valueMessageDesc be ? value.[GetOwnProperty].
- Let message be undefined if IsDataDescriptor(valueMessageDesc) is false, and ? ToString(valueMessageDesc.[[Value]]) otherwise.
- Set serialized to { : "Error", [[Name]]: name, [[Message]]: message }.
- User agents should attach a serialized representation of any interesting accompanying data which are not yet specified, notably the
stack
property, to serialized. See the Error Stacks proposal for in-progress work on specifying this data. [JSERRORSTACKS]
Otherwise, if value is an Array exotic object, then:
- Let valueLenDescriptor be ? OrdinaryGetOwnProperty(value, "
length
"). - Let valueLen be valueLenDescriptor.[[Value]].
- Set serialized to { : "Array", [[Length]]: valueLen, [[Properties]]: a new empty List }.
- Set deep to true.
- Let valueLenDescriptor be ? OrdinaryGetOwnProperty(value, "
Otherwise, if value is a platform object that is a serializable object:
- If value has a [[Detached]] internal slot whose value is true, then throw a "
DataCloneError
"<a data-x-internal="domexception" href="https://webidl.spec.whatwg.org/#dfn-DOMException">DOMException</a>
. - Let typeString be the identifier of the primary interface of value.
- Set serialized to { : typeString }.
- Set deep to true.
- If value has a [[Detached]] internal slot whose value is true, then throw a "
Otherwise, if value is a platform object, then throw a "
DataCloneError
"<a data-x-internal="domexception" href="https://webidl.spec.whatwg.org/#dfn-DOMException">DOMException</a>
.Otherwise, if IsCallable(value) is true, then throw a "
DataCloneError
"<a data-x-internal="domexception" href="https://webidl.spec.whatwg.org/#dfn-DOMException">DOMException</a>
.Otherwise, if value has any internal slot other than [[Prototype]], [[Extensible]], or [[PrivateElements]], then throw a "
DataCloneError
"<a data-x-internal="domexception" href="https://webidl.spec.whatwg.org/#dfn-DOMException">DOMException</a>
. For instance, a [[PromiseState]] or [[WeakMapData]] internal slot.Otherwise, if value is an exotic object and value is not the %Object.prototype% intrinsic object associated with any realm, then throw a "
DataCloneError
"<a data-x-internal="domexception" href="https://webidl.spec.whatwg.org/#dfn-DOMException">DOMException</a>
. For instance, a proxy object.Otherwise:
- Set serialized to { : "Object", [[Properties]]: a new empty List }.
- Set deep to true.
%Object.prototype% will end up being handled via this step and subsequent steps. The end result is that its exoticness is ignored, and after deserialization the result will be an empty object (not an immutable prototype exotic object).
Set memory[value] to serialized.
If deep is true, then:
- If value has a [[MapData]] internal slot, then:
- Let copiedList be a new empty List.
- For each Record { [[Key]], [[Value]] } entry of value.[[MapData]]:
- For each Record { [[Key]], [[Value]] } entry of copiedList:
- Let serializedKey be ? StructuredSerializeInternal(entry.[[Key]], forStorage, memory).
- Let serializedValue be ? StructuredSerializeInternal(entry.[[Value]], forStorage, memory).
- Append { [[Key]]: serializedKey, [[Value]]: serializedValue } to serialized.[[MapData]].
- Otherwise, if value has a [[SetData]] internal slot, then:
- Let copiedList be a new empty List.
- For each entry of value.[[SetData]]:
- If entry is not the special value empty , append entry to copiedList.
- For each entry of copiedList:
- Let serializedEntry be ? StructuredSerializeInternal(entry, forStorage, memory).
- Append serializedEntry to serialized.[[SetData]].
- Otherwise, if value is a platform object that is a serializable object, then perform the serialization steps for value's primary interface, given value, serialized, and forStorage. The serialization steps may need to perform a sub-serialization. This is an operation which takes as input a value subValue, and returns StructuredSerializeInternal(subValue, forStorage, memory). (In other words, a sub-serialization is a specialization of StructuredSerializeInternal to be consistent within this invocation.)
- Otherwise, for each key in ! EnumerableOwnProperties(value, key):
- If ! HasOwnProperty(value, key) is true, then:
- Let inputValue be ? value.[[Get]](key, value).
- Let outputValue be ? StructuredSerializeInternal(inputValue, forStorage, memory).
- Append { [[Key]]: key, [[Value]]: outputValue } to serialized.[[Properties]].
- If ! HasOwnProperty(value, key) is true, then:
- If value has a [[MapData]] internal slot, then:
Return serialized.
It's important to realize that the Records produced by StructuredSerializeInternal might contain "pointers" to other records that create circular references. For example, when we pass the following JavaScript object into StructuredSerializeInternal: